# -*- coding: utf-8 -*-
"""
Created on Tue Nov 29 13:28:22 2022

@author: sthgy
"""


import os
import re
import datetime
import numpy as np
import pandas as pd
from scipy import optimize
from matplotlib import pyplot as plt

from utils import Fund, date_format
from description import read_funds_sifted
from utils import calc_annual_return, calc_annual_volatility,\
    calc_annual_dd_vol, calc_drawdown

''' 前提假设：
1. 净值为（管理费、托管费）的费后收益，已经在收益率中体现
2. 长期持有，不考虑申赎产生的费用
'''

plt.rcParams["font.sans-serif"]=["SimHei"] #设置字体
plt.rcParams["axes.unicode_minus"]=False #该语句解决图像中的“-”负号的乱码问题
plt.rcParams.update({"font.size":20})#此处必须添加此句代码方可改变标题字体大小

def frontier():
    ''' 计算并绘制收益率-波动率的有效边界 '''
    funds = read_funds_sifted()
    rets_list = []
    for k, v in funds.items():
        fund_ret = v.fund_ret
        fund_ret = fund_ret.loc[
            (fund_ret['日期']>=date_format('2017-01-01'))&
            (fund_ret['日期']<=date_format('2021-12-31')),
            :].reset_index(drop=True)
        fund_ret['基金代码'] = np.repeat(k, fund_ret.shape[0])
        rets_list.append(fund_ret)
    rets = pd.concat(rets_list, ignore_index=True).pivot(
        index='日期',columns='基金代码',values='收益率')
    
    out_mean, out_std, out_weight = [], [], []
    cov_annual = rets.cov()
    ret_annual = rets.mean()
    fail_r = []
    n_fund = rets.shape[1]
    W0 = np.ones([n_fund])/n_fund
    r = ret_annual.min()
    b_ = [(0,1) for i in range(n_fund)] # 不允许做空
    c_ = ({'type':'eq', 'fun': lambda W: sum(W)-1. }) # 权重归一
    count = 1
    def objFunction(W):
        # 大概是拉格朗日乘子吧
        target_ret = r
        port_ret = np.dot(W, ret_annual)
        port_var = np.dot(np.dot(W, cov_annual), W.T)
        return np.sqrt(port_var) + 2000 * abs(port_ret - target_ret)
    print(datetime.datetime.now())
    for r in np.linspace(max(ret_annual.min(), 0), ret_annual.max(), num=300)[1:-1]:
        print(f'{count}  {datetime.datetime.now()}')
        result = optimize.minimize(
            fun = objFunction,
            x0 = W0,
            method='SLSQP',
            constraints=c_,
            bounds=b_)
        if not result.success:
            # raise BaseException(result.message)
            fail_r.append(r)
        std_ = np.dot(rets, result.x).std()/np.sqrt(250)
        out_mean.append(r)
        out_std.append(std_)
        out_weight.append(result.x)
        count += 1
    print(datetime.datetime.now())
    effective = pd.DataFrame()
    effective['目标收益率'] = out_mean.copy()
    effective['最小波动率'] = out_std.copy()
    effective['组合权重'] = out_weight.copy()
    with pd.HDFStore('./FundData/data.h5') as hdf_file:
        rets.to_hdf(hdf_file, 'ret_pivot')
        effective.to_hdf(hdf_file, 'frontier')

def draw_frontier():
    ''' 绘制有效边界 '''
    with pd.HDFStore('./FundData/data.h5') as hdf_file:
        effective = pd.read_hdf(hdf_file, 'frontier')
    funds = read_funds_sifted()
    description = pd.DataFrame()
    description['年化收益率'] = [x.annual_return for x in funds.values()]
    description['年化波动率'] = [x.annual_volatility for x in funds.values()]
    fig, ax = plt.subplots(figsize=(20, 20))
    ax.plot(effective['目标收益率'], effective['最小波动率'], '-')
    ax.plot(description['年化收益率'], description['年化波动率'], 'o')
    ax.set_xlabel('年化收益率')
    ax.set_ylabel('年化波动率')
    ax.grid(True)
    # plt.show()
    if not os.path.exists('./Image'):
        os.makedirs('./Image')
    plt.savefig('./Image/Frontier.jpg')
    plt.close()

def performance():
    ''' 基金评价
    由于筛选数量仍较多，计算过于费时，因此需要进一步优选，缩减基金标的个数
    评价目前依赖于业绩加权排名与指标排名
    （开摆）持仓风格/杠杆比涉及大量历史数据，有空再补吧
    '''
    funds = read_funds_sifted()
    description = pd.DataFrame()
    description['基金代码'] = [x.fund_code for x in funds.values()]
    description['基金名称'] = [x.fund_name for x in funds.values()]
    description['基金类型'] = [x.fund_type for x in funds.values()]
    description['债券型'] = description['基金类型'].apply(
        lambda x: '债' in x)
    description['基金规模'] = [x.fund_scale for x in funds.values()]
    description['净值起始日期'] = [x.start_date for x in funds.values()]
    description['净值终止日期'] = [x.end_date for x in funds.values()]
    description['年化收益率'] = [x.annual_return for x in funds.values()]
    description['年化波动率'] = [x.annual_volatility for x in funds.values()]
    description['年化下行波动率'] = [x.annual_dd_vol for x in funds.values()]
    description['最大回撤'] = [x.drawdown for x in funds.values()]
    description['最大回撤期起始日'] = [x.drawdown_start for x in funds.values()]
    description['最大回撤期终止日'] = [x.drawdown_end for x in funds.values()]
    description['夏普比率'] = [x.sharpe for x in funds.values()]
    description['所提诺比率'] = [x.sortino for x in funds.values()]
    description['卡玛比率'] = [x.calmar for x in funds.values()]
    weightedrank = {}
    for group, data in description.groupby(by=['债券型']):
        monthlyret = pd.DataFrame()
        for fund_code in data['基金代码'].values:
            fund_ret = funds[fund_code].fund_ret
            fund_ret = fund_ret.loc[
                (fund_ret['日期']>=date_format('2017-01-01'))&
                (fund_ret['日期']<=date_format('2021-12-31')),
                :].reset_index(drop=True)
            fund_ret['季度'] = fund_ret['日期'].apply(
                lambda x: x.year*100+(x.month-1)//3+1)
            fund_ret['收益率+1'] = fund_ret['收益率'] + 1
            fund_monthly = fund_ret.groupby(by=['季度'], as_index=False).agg(
                {'收益率+1':np.prod})
            fund_monthly['季度排序'] = fund_monthly['季度'].rank(ascending=False)
            fund_monthly[fund_code] = fund_monthly['收益率+1'] - 1
            monthlyret = monthlyret.merge(
                fund_monthly[['季度排序',fund_code]].set_index(
                    keys=['季度排序'], drop=True),
                how='outer', left_index=True, right_index=True)
        monthlyrank = monthlyret.T.rank(pct=True, ascending=False).T
        # 一年（4个季度）为半衰期
        halflife = 0.5 ** (monthlyrank.index.values / 4)
        for fund_code in data['基金代码'].values:
            weightedrank[fund_code] = np.sqrt(
                np.sum(halflife * monthlyrank[fund_code].values**2) /
                np.sum(halflife))
    description['加权排名'] = description['基金代码'].apply(
        lambda x: weightedrank[x]) # 实际上衡量的是业绩排名稳定性
    rank_list = [] # 打分，越大越好
    for group, data in description.groupby(by=['债券型']):
        type_rank = data.copy()
        type_rank['收益能力'] = type_rank['年化收益率'].rank(
            pct=True, ascending=True)
        type_rank['风险控制'] = type_rank['年化下行波动率'].rank(
            pct=True, ascending=False)
        type_rank['极端损失'] = type_rank['最大回撤'].rank(
            pct=True, ascending=True)
        type_rank['业绩稳定'] = type_rank['加权排名'].rank(
            pct=True, ascending=False)
        type_rank['综合得分'] = (
            type_rank['收益能力'] + type_rank['风险控制'] +
            type_rank['极端损失'] + type_rank['业绩稳定']
            ).rank(pct=True, ascending=True)
        rank_list.append(type_rank)
    rank = pd.concat(rank_list, ignore_index=True)
    return rank

def max_sharpe(n_target=15, max_weight=0.2, min_ret=0.1):
    ''' 最优组合权重
    n_target: int, 目标基金个数
    n_pool: int, 基金优选池个数
    max_bond: int, 最大债券型基金数量
    max_weight: float, 0~1, 单个基金最大权重
    min_ret: float, 组合最低年化收益率（非百分百）
    max_dd: float, 容忍的最大回撤绝对值
    '''
    start_date = date_format('2017-01-01')
    end_date = date_format('2021-12-31')
    rank_raw = performance()
    # 去掉同个基金不同的份额，避免相关性过高
    rank_raw['基金名称去份额'] = rank_raw['基金名称'].apply(
        lambda x: x.replace('(LOF)','').replace('A/B','')).apply(
            lambda x: x.replace(([''] + re.findall(
                '[A-Z]',x.replace('ETF', '')))[-1], ''))
    rank = rank_raw.sort_values(
        by=['基金名称']).groupby(
            by=['基金名称去份额']).first().reset_index(drop=True)
    funds = read_funds_sifted()
    pool_list = list(rank.sort_values(
        by=['综合得分'], ascending=False
        ).head(n_target)['基金代码'].values)
    rets_list = []
    for fund_code in pool_list:
        fund_ret = funds[fund_code].fund_ret
        fund_ret = fund_ret.loc[
            (fund_ret['日期']>=start_date)&
            (fund_ret['日期']<=end_date),
            :].reset_index(drop=True)
        fund_ret['基金代码'] = np.repeat(fund_code, fund_ret.shape[0])
        rets_list.append(fund_ret)
    rets = pd.concat(rets_list, ignore_index=True).pivot(
        index='日期',columns='基金代码',values='收益率')
    cov = rets.cov() * 250
    mean = rets.mean() * 250
    bounds = optimize.Bounds(
        np.repeat(0, n_target),
        np.repeat(max_weight, n_target))
    # 自变量
    w0 = np.repeat(1 / n_target, n_target)
    def Sharpe(w):
        port_ret = np.dot(w, mean)
        port_vol = np.sqrt(np.dot(np.dot(w, cov), w))
        return - port_ret / port_vol
    res = optimize.minimize(
        fun=Sharpe,
        x0=w0,
        method='SLSQP',
        constraints=[
            {'type':'eq', 'fun': lambda w: np.sum(w) - 1},
            {'type':'ineq', 'fun':lambda w: np.dot(w, mean) - 0.1}
            ],
        options={'xatol': 1e-20, 'disp': True},
        bounds=bounds)
    result = {}
    for i in range(0, n_target):
        fund_code = pool_list[i]
        weight = res.x[i]
        result[fund_code] = weight
        print(f'{fund_code}  {weight:.6f}  {funds[fund_code].fund_name}')
    return result

class Port:
    ''' 组合收益率序列与指标 '''
    def __init__(self, weight,
                 insample_start=date_format('2017-01-01'),
                 insample_end=date_format('2021-12-31'),
                 outsample_start=date_format('2022-01-01'),
                 outsample_end=date_format('2022-12-31')):
        funds = read_funds_sifted()
        insample = pd.DataFrame(columns=['日期'])
        outsample = pd.DataFrame(columns=['日期'])
        for k, v in weight.items():
            fund_ret = funds[k].fund_ret
            insample_part = fund_ret.loc[
                (fund_ret['日期']>=insample_start)&
                (fund_ret['日期']<=insample_end),
                :].reset_index(drop=True).rename(columns={'收益率':k})
            outsample_part = fund_ret.loc[
                (fund_ret['日期']>=outsample_start)&
                (fund_ret['日期']<=outsample_end),
                :].reset_index(drop=True).rename(columns={'收益率':k})
            insample = pd.merge(
                insample, insample_part, how='outer',
                on=['日期'])
            outsample = pd.merge(
                outsample, outsample_part, how='outer',
                on=['日期'])
        insample.fillna(0).sort_values(by=['日期'], inplace=True)
        self.insample = pd.DataFrame()
        self.insample['日期'] = insample['日期']
        self.insample['收益率'] = np.repeat(0, insample.shape[0])
        for k, v in weight.items():
            self.insample['收益率'] += insample[k] * v
        self.outsample = pd.DataFrame()
        self.outsample['日期'] = outsample['日期']
        self.outsample['收益率'] = np.repeat(0, outsample.shape[0])
        for k, v in weight.items():
            self.outsample['收益率'] += outsample[k] * v
        self.annual_return_in = calc_annual_return(
            self.insample, insample_start, insample_end)
        self.annual_volatility_in = calc_annual_volatility(
            self.insample, insample_start, insample_end)
        self.annual_dd_vol_in = calc_annual_dd_vol(
            self.insample, insample_start, insample_end)
        self.drawdown_in, self.drawdown_start_in, self.drawdown_end_in = \
            calc_drawdown(self.insample, insample_start, insample_end)
        self.annual_return_out = calc_annual_return(
            self.outsample, outsample_start, outsample_end)
        self.annual_volatility_out = calc_annual_volatility(
            self.outsample, outsample_start, outsample_end)
        self.annual_dd_vol_out = calc_annual_dd_vol(
            self.outsample, outsample_start, outsample_end)
        self.drawdown_out, self.drawdown_start_out, self.drawdown_end_out = \
            calc_drawdown(self.outsample, outsample_start, outsample_end)
        
